#!/usr/bin/env bash

#####################################################################
#
# Copyright (C) NGINX, Inc.
# Author: NGINX Unit Team, F5 Inc.
# Copyright 2024, Alejandro Colomar <alx@kernel.org>
#
#####################################################################


if test -n ${BASH_VERSION} && test "${BASH_VERSINFO[0]}" -eq 3; then
    >&2 cat <<__EOF__ ;
Your version of bash(1) isn't supported by this script.  You're probably
running on macOS.  We recommend that you either install a newer version
of bash(1) or run this script with another shell, such as zsh(1):

    $ ${SUDO_USER:+sudo }zsh $0 ...
__EOF__
    exit 1;
fi;


set -Eefuo pipefail;

test -v BASH_VERSION \
&& shopt -s lastpipe;

test -v ZSH_VERSION \
&& setopt sh_word_split;


export LC_ALL=C

dry_run='no';

help_unit()
{
    cat <<__EOF__ ;
SYNOPSIS
        $0 [-h[h]] COMMAND [ARGS]

    Subcommands
        ├── repo-config  [-hn] [PKG-MANAGER OS-NAME OS-VERSION]
        └── welcome      [-hn]

DESCRIPTION
        This script simplifies installing and configuring an NGINX Unit server
        for first-time users.

        Run '$0 COMMAND -h' for more information on a command.

COMMANDS
        repo-config
                Configure your package manager with the NGINX Unit repository
                for later installation.

        welcome
                Create an initial configuration to serve a welcome web page
                with NGINX Unit.

OPTIONS
        -h, --help
                Print this help.

        -hh, --help-more
                Print help for more (advanced) commands.

__EOF__
}

help_more_unit()
{
    cat <<__EOF__ ;
SYNOPSIS
        $0 [-h[h]] COMMAND [ARGS]

    Subcommands
        ├── cmd          [-h]
        ├── ctl          [-h] [-s SOCK] SUBCOMMAND [ARGS]
        │   ├── edit     [-h] PATH
        │   ├── http     [-h] [-c CURLOPT] METHOD PATH
        │   └── insert   [-h] PATH INDEX
        ├── freeport     [-h]
        ├── json-ins     [-hn] JSON INDEX
        ├── os-probe     [-h]
        ├── ps           [-h] [-t TYPE]
        ├── repo-config  [-hn] [PKG-MANAGER OS-NAME OS-VERSION]
        ├── restart      [-hls]
        ├── sock         [-h] SUBCOMMAND [ARGS]
        │   ├── filter   [-chs]
        │   └── find     [-h]
        └── welcome      [-hn]

DESCRIPTION
        This script simplifies installing and configuring
        an NGINX Unit server for first-time users.

        Run '$0 COMMAND -h' for more information on a command.

COMMANDS
        cmd     Print the invocation line of unitd(8).

        ctl     Control a running unitd(8) instance via its control API socket.

        freeport
                Print an available TCP port.

        json-ins
                Insert a JSON element read from standard input into a JSON
                array read from a file at a given INDEX.

        os-probe
                Probe the OS and print details about its version.

        ps      List unitd(8) processes.

        repo-config
                Configure your package manager with the NGINX Unit
                repository for later installation.

        restart
                Restart all running unitd(8) instances.

        sock    Print the control API socket address.

        welcome
                Create an initial configuration to serve a welcome web page
                with NGINX Unit.

OPTIONS
        -h, --help
                Print basic help (some commands are hidden).

        -hh, --help-more
                Print the hidden help with more commands.

__EOF__
}

info()
{
    >&2 echo "$(basename "$0"): info: $*";
}

warn()
{
    >&2 echo "$(basename "$0"): error: $*";
}

err()
{
    >&2 echo "$(basename "$0"): error: $*";
    exit 1;
}

dry_run_echo()
{
    if test "$dry_run" = "yes"; then
        echo "$*";
    fi;
}

dry_run_eval()
{
    if test "$dry_run" = "yes"; then
        echo "    $*";
    else
        eval "$*";
    fi;
}

run_trap()
{
    trap -p "$1" \
    | tr -d '\n' \
    | sed "s/[^']*'\(.*\)'[^']*/\1/" \
    | sed "s/'\\\\''/'/g" \
    | read -r trap_cmd;

    eval $trap_cmd;
    trap - "$1";
}


help_unit_cmd()
{
    cat <<__EOF__ ;
SYNOPSIS
        $0 cmd [-h]

DESCRIPTION
        Print the invocation line of running unitd(8) instances.

OPTIONS
        -h, --help
                Print this help.

__EOF__
}


unit_cmd()
{
    while test $# -ge 1; do
        case "$1" in
        -h | --help)
            help_unit_cmd;
            exit 0;
            ;;
        -*)
            err "cmd: $1: Unknown option.";
            ;;
        *)
            err "cmd: $1: Unknown argument.";
            ;;
        esac;
        shift;
    done;

    unit_ps -t m \
    | sed 's/.*\[\(.*\)].*/\1/';
}


help_unit_ctl()
{
    cat <<__EOF__ ;
SYNOPSIS
        $0 ctl [-h] [-s SOCK] SUBCOMMAND [ARGS]

    Subcommands
        ├── edit    [-h] PATH
        ├── http    [-h] [-c CURLOPT] METHOD PATH
        └── insert  [-h] PATH INDEX

DESCRIPTION
        Control a running unitd(8) instance through its control API socket.

        Run '$0 ctl SUBCOMMAND -h' for more information on a
        subcommand.

SUBCOMMANDS
        edit    Edit the unitd(8) configuration with an editor.

        http    Send an HTTP request to the control API socket.

        insert  Insert an element at the specified index into an array in the
                JSON configuration.

OPTIONS
        -h, --help
                Print this help.

        -s, --sock SOCK
                Use SOCK as the control API socket address.  If not specified,
                the script tries to find it.  This value is used by subcommands.

                The socket can be a tcp(7) socket or a unix(7) socket; in
                the case of a unix(7) socket, it can exist locally or on
                a remote machine, accessed through ssh(1).  Accepted syntax
                for SOCK:

                    unix:/path/to/control.sock
                    ssh://[user@]host[:port]/path/to/control.sock
                    [http[s]://]host[:port]

                The last form is less secure than the first two; have a look:
                <https://unit.nginx.org/howto/security/#secure-socket-and-stat>

ENVIRONMENT
        Options take precedence over their equivalent environment variables;
        if both are specified, the command-line option is used.

        UNIT_CTL_SOCK
                Equivalent to the option -s (--sock).

__EOF__
}


unit_ctl()
{

    if test -v UNIT_CTL_SOCK; then
        local sock="$UNIT_CTL_SOCK";
    fi;

    while test $# -ge 1; do
        case "$1" in
        -h | --help)
            help_unit_ctl;
            exit 0;
            ;;
        -s | --sock)
            if ! test $# -ge 2; then
                err "ctl: $1: Missing argument.";
            fi;
            local sock="$2";
            shift;
            ;;
        -*)
            err "ctl: $1: Unknown option.";
            ;;
        *)
            break;
            ;;
        esac;
        shift;
    done;

    if test ! $# -ge 1; then
        err 'ctl: Missing subcommand.';
    fi;

    if ! test -v sock; then
        local sock="$(unit_sock_find)";
    fi;

    if echo $sock | grep '^ssh://' >/dev/null; then
        local remote="$(echo $sock | sed 's,\(ssh://[^/]*\).*,\1,')";
        local sock="$(echo $sock | sed 's,ssh://[^/]*\(.*\),unix:\1,')";

        local remote_sock="$(echo "$sock" | unit_sock_filter -s)";
        local local_sock="$(mktemp -u -p /var/run/unit/)";
        local ssh_ctrl="$(mktemp -u -p /var/run/unit/)";

        mkdir -p /var/run/unit/;

        ssh -fMNnT -S "$ssh_ctrl" \
                   -o 'ExitOnForwardFailure yes' \
                   -L "$local_sock:$remote_sock" "$remote";

        trap "ssh -S '$ssh_ctrl' -O exit '$remote' 2>/dev/null;
              unlink '$local_sock';" EXIT;

        sock="unix:$local_sock";
    fi;

    case $1 in
    edit)
        shift;
        unit_ctl_edit ---s "$sock" $@;
        ;;
    http)
        shift;
        unit_ctl_http ---s "$sock" $@;
        ;;
    insert)
        shift;
        unit_ctl_insert ---s "$sock" $@;
        ;;
    *)
        err "ctl: $1: Unknown argument.";
        ;;
    esac;

    if test -v remote; then
        run_trap EXIT;
    fi;
}


help_unit_ctl_edit()
{
    cat <<__EOF__ ;
SYNOPSIS
        $0 ctl [CTL-OPTS] edit [-h] PATH

DESCRIPTION
        Edit the JSON configuration with an editor.  The current configuration
        is downloaded into a temporary file, open with the editor, and then
        sent back to the control API socket.

        The following editors are tried in this order of preference: \$VISUAL,
        \$EDITOR, editor(1), vi(1), vim(1), ed(1).


OPTIONS
        -h, --help
                Print this help.

ENVIRONMENT
        VISUAL
        EDITOR
                See environ(7).

SEE ALSO
        $0 ctl http -h;

        update-alternatives(1)

__EOF__
}


unit_ctl_edit()
{
    while test $# -ge 1; do
        case "$1" in
        -h | --help)
            help_unit_ctl_edit;
            exit 0;
            ;;
        ---s | ----sock)
            local sock="$2";
            shift;
            ;;
        -*)
            err "ctl: edit: $1: Unknown option.";
            ;;
        *)
            break;
            ;;
        esac;
        shift;
    done;

    if ! test $# -ge 1; then
        err 'ctl: edit: PATH: Missing argument.';
    fi;
    local req_path="$1";

    echo "$req_path" \
    | sed 's%^/js_modules/.*%.js%' \
    | sed 's%^/config\>.*%.json%' \
    | sed 's%^/.*%.txt%' \
    | xargs mktemp --suffix \
    | read -r tmp;

    unit_ctl_http ---s "$sock" -c --no-progress-meter GET "$req_path" \
            </dev/null >"$tmp";

    $(
        ((test -v VISUAL && test -n "$VISUAL") && printf '%s\n' "$VISUAL") \
        || ((test -v EDITOR && test -n "$EDITOR") && printf '%s\n' "$EDITOR") \
        || command -v editor \
        || command -v vi \
        || command -v vim \
        || echo ed;
    ) "$tmp";

    trap "info 'ctl: edit: Invalid configuration saved in <$tmp>.'" ERR

    unit_ctl_http ---s "$sock" PUT "$req_path" <"$tmp";

    trap - ERR;
}


help_unit_ctl_http()
{
    cat <<__EOF__ ;
SYNOPSIS
        $0 ctl [CTL-OPTS] http [-h] [-c CURLOPT] METHOD PATH

DESCRIPTION
        Send an HTTP request to the unitd(8) control API socket.

        The payload is read from standard input.

OPTIONS
        -c, --curl CURLOPT
                Pass CURLOPT as an option to curl.  This script is implemented
                in terms of curl(1), so it's useful to be able to tweak its
                behavior.  The option can be cumulatively used multiple times
                (the result is also appended to UNIT_CTL_HTTP_CURLOPTS).

        -h, --help
                Print this help.

ENVIRONMENT
        UNIT_CTL_HTTP_CURLOPTS
                Equivalent to the option -c (--curl).

EXAMPLES
        $0 ctl http -c --no-progress-meter GET /config >tmp;

SEE ALSO
        <https://unit.nginx.org/controlapi/#api-manipulation>

__EOF__
}


unit_ctl_http()
{
    local curl_options="${UNIT_CTL_HTTP_CURLOPTS:-}";

    while test $# -ge 1; do
        case "$1" in
        -c | --curl)
            if ! test $# -ge 2; then
                err "ctl: http: $1: Missing argument.";
            fi;
            curl_options="$curl_options $2";
            shift;
            ;;
        -h | --help)
            help_unit_ctl_http;
            exit 0;
            ;;
        ---s | ----sock)
            local sock="$2";
            shift;
            ;;
        -*)
            err "ctl: http: $1: Unknown option.";
            ;;
        *)
            break;
            ;;
        esac;
        shift;
    done;

    if ! test $# -ge 1; then
        err 'ctl: http: METHOD: Missing argument.';
    fi;
    local method="$1";

    if ! test $# -ge 2; then
        err 'ctl: http: PATH: Missing argument.';
    fi;
    local req_path="$2";

    curl --fail-with-body $curl_options -X $method -d@- \
            $(echo "$sock" | unit_sock_filter -c)${req_path};
}


help_unit_ctl_insert()
{
    cat <<__EOF__ ;
SYNOPSIS
        $0 ctl [CTL-OPTS] insert [-h] PATH INDEX

DESCRIPTION
        Insert an element at the specified position (INDEX) into the JSON array
        located at PATH in unitd(8) control API.

        The new element is read from standard input.

OPTIONS
        -h, --help
                Print this help.

SEE ALSO
        $0 ctl http -h;

__EOF__
}


unit_ctl_insert()
{
    while test $# -ge 1; do
        case "$1" in
        -h | --help)
            help_unit_ctl_insert;
            exit 0;
            ;;
        ---s | ----sock)
            local sock="$2";
            shift;
            ;;
        -*)
            err "ctl: insert: $1: Unknown option.";
            ;;
        *)
            break;
            ;;
        esac;
        shift;
    done;

    if ! test $# -ge 1; then
        err 'ctl: insert: PATH: Missing argument.';
    fi;
    local req_path="$1";

    if ! test $# -ge 2; then
        err 'ctl: insert: INDEX: Missing argument.';
    fi;
    local idx="$2";

    local old="$(mktemp)";

    unit_ctl_http ---s "$sock" -c --no-progress-meter GET "$req_path" \
            </dev/null >"$old";

    unit_json_ins "$old" "$idx" \
    | unit_ctl_http ---s "$sock" PUT "$req_path";
}


help_unit_ctl_welcome()
{
    cat <<__EOF__ ;
SYNOPSIS
        $0 welcome [-hn]

DESCRIPTION
        This script tests an NGINX Unit installation by creating an initial
        configuration and serving a welcome web page.  Recommended for
        first-time users.

OPTIONS
        -h, --help
                Print this help.

        -n, --dry-run
                Dry run.  Print the commands to be run instead of actually
                running them.  Each command is preceded by a line explaining
                what it does.

__EOF__
}


unit_ctl_welcome()
{
    while test $# -ge 1; do
        case "$1" in
        -f | --force)
            local force='yes';
            ;;
        -h | --help)
            help_unit_ctl_welcome;
            exit 0;
            ;;
        -n | --dry-run)
            dry_run='yes';
            ;;
        -*)
            err "welcome: $1: Unknown option.";
            ;;
        *)
            err "welcome: $1: Unknown argument.";
            ;;
        esac;
        shift;
    done;

    command -v curl >/dev/null \
    || err 'welcome: curl(1) not found in PATH.  It must be installed to run this script.';

    www='/srv/www/unit/index.html';
    if test -e "$www" && ! test -v force || ! test -w /srv; then
        www="$HOME/srv/www/unit/index.html";
    fi;
    if test -e "$www" && ! test -v force; then
        www="$(mktemp)";
        mv "$www" "$www.html";
        www="$www.html"
    fi;

    unit_ps -t m \
    | wc -l \
    | read -r nprocs \
    ||:

    if test 0 -eq "$nprocs"; then
        warn "welcome: NGINX Unit isn't running.";
        warn 'For help with starting NGINX Unit, see:';
        err  "  <https://unit.nginx.org/installation/#startup-and-shutdown>";
    elif test 1 -ne "$nprocs"; then
        err 'welcome: Only one NGINX Unit instance should be running.';
    fi;

    local sock="$(unit_sock_find)";
    local curl_opt="$(unit_sock_find | unit_sock_filter -c)";

    curl $curl_opt/ >/dev/null 2>&1 \
    || err "welcome: Can't reach the control API socket.";

    if ! test -v force; then
        unit_cmd \
        | read -r cmd;

        # Check unitd is not configured already.
        echo "$cmd" \
        | if grep '\--statedir' >/dev/null; then
            echo "$cmd" \
            | sed 's/ --/\n--/g' \
            | grep '\--statedir' \
            | cut -d' ' -f2;
        else
            $cmd --help \
            | sed -n '/\--statedir/,+1p' \
            | grep 'default:' \
            | sed 's/ *default: "\(.*\)"/\1/';
        fi \
        | sed 's,$,/conf.json,' \
        | read -r conffile \
        ||:;

        if test -e $conffile; then
            if ! unit_ctl_http ---s "$sock" 'GET' '/config' </dev/null 2>/dev/null | grep -q '^{}.\?$';  # The '.\?' is for the possible carriage return.
            then
                warn 'welcome: NGINX Unit is already configured.  To overwrite';
                err  'its current configuration, run the script again with --force.';
            fi;
        fi;
    fi;

    (
        unit_freeport \
        || err "welcome: Can't find an available port.";
    ) \
    | read -r port;

    dry_run_echo 'Create a file to serve:';
    dry_run_eval "mkdir -p $(dirname $www);";
    dry_run_eval "tee '$www' >/dev/null"' <<__EOF__;
        <!DOCTYPE html>
        <html>
            <head>
                <title>Welcome to NGINX Unit</title>
                <style type="text/css">
                    body { background: white; color: black; font-family: sans-serif; margin: 2em; line-height: 1.5; }
                    h1,h2 { color: #00974d; }
                    li { margin-bottom: 0.5em; }
                    pre { background-color: beige; padding: 0.4em; }
                    hr { margin-top: 2em; border: 1px solid #00974d; }
                    .indent { margin-left: 1.5em; }
                </style>
            </head>
            <body>
                <h1>Welcome to NGINX Unit</h1>
                <p>Congratulations! NGINX Unit is installed and running.</p>
                <h3>Useful Links</h3>
                <ul>
                    <li><b><a href="https://unit.nginx.org/configuration/?referer=welcome">https://unit.nginx.org/configuration/</a></b><br>
                        To get started with Unit, see the <em>Configuration</em> docs, starting with
                        the <em>Quick Start</em> guide.</li>
                    <li><b><a href="https://github.com/nginx/unit">https://github.com/nginx/unit</a></b><br>
                        See our GitHub repo to browse the code, contribute, or seek help from the
                        <a href="https://github.com/nginx/unit#community">community</a>.</li>
                </ul>

                <h2>Next steps</h2>

                <h3>Check Current Configuration</h3>
                <div class="indent">
                <p>Unit'"'"'s control API is currently listening for configuration changes
                   on the '"$(unit_sock_find | grep -q '^unix:' && echo '<a href="https://en.wikipedia.org/wiki/Unix_domain_socket">Unix socket</a>' || echo 'socket')"' at
                   <b>'"$(unit_sock_find)"'</b><br>
                   To see the current configuration:</p>
                <pre>'"${SUDO_USER:+sudo }"'curl '"$curl_opt"'/config</pre>
                </div>

                <h3>Change Listener Port</h3>
                <div class="indent">
                <p>This page is served over a random TCP high port.  To choose the default HTTP port (80),
                   replace the <b>"listeners"</b> object:</p>
                <pre>echo '"'"'{"*:80": {"pass": "routes"}}'"'"' | '"${SUDO_USER:+sudo }"'curl -X PUT -d@- '"$curl_opt"'/config/listeners</pre>
                Then remove the port number from the address bar and reload the page.
                </div>

                <hr>
                <p><a href="https://unit.nginx.org/?referer=welcome">NGINX Unit &mdash; the universal web app server</a><br>
                NGINX, Inc. &copy; 2025</p>
            </body>
        </html>
__EOF__';
    dry_run_echo;
    dry_run_echo 'Give it appropriate permissions:';
    dry_run_eval "chmod 644 '$www';";
    dry_run_echo;

    dry_run_echo 'Configure unitd:'
    dry_run_eval "cat <<__EOF__ \\
        | sed 's/8080/$port/' \\
        | curl -X PUT -d@- $curl_opt/config;
        {
            \"listeners\": {
                \"*:8080\": {
                    \"pass\": \"routes\"
                }
            },
            \"routes\": [{
                \"action\": {
                    \"share\": \"$www\"
                }
            }]
        }
__EOF__";

    dry_run_echo;

    echo;
    echo 'You may want to try the following commands now:';
    echo;
    echo 'Check out current unitd configuration:';
    echo "    ${SUDO_USER:+sudo} curl $curl_opt/config";
    echo;
    echo 'Browse the welcome page:';
    echo "    curl http://localhost:$port/";
}


help_unit_freeport()
{
    cat <<__EOF__ ;
SYNOPSIS
        $0 freeport [-h]

DESCRIPTION
        Print an available TCP port.

OPTIONS
        -h, --help
                Print this help.

__EOF__
}


unit_freeport()
{
    while test $# -ge 1; do
        case "$1" in
        -h | --help)
            help_unit_freeport;
            exit 0;
            ;;
        -*)
            err "freeport: $1: Unknown option.";
            ;;
        *)
            err "freeport: $1: Unknown argument.";
            ;;
        esac;
        shift;
    done;

    freeport="$(mktemp -t freeport-XXXXXX)";

    cat <<__EOF__ \
    | cc -x c -o $freeport -;
        #include <netinet/in.h>
        #include <stdio.h>
        #include <stdlib.h>
        #include <strings.h>
        #include <sys/socket.h>
        #include <unistd.h>


        int32_t get_free_port(void);


        int
        main(void)
        {
            int32_t  port;

            port = get_free_port();
            if (port == -1)
                exit(EXIT_FAILURE);

            printf("%d\n", port);
            exit(EXIT_SUCCESS);
        }


        int32_t
        get_free_port(void)
        {
            int                 sfd;
            int32_t             port;
            socklen_t           len;
            struct sockaddr_in  addr;

            port = -1;

            sfd = socket(PF_INET, SOCK_STREAM, 0);
            if (sfd == -1) {
                perror("socket()");
                return -1;
            }

            bzero(&addr, sizeof(addr));
            addr.sin_family = AF_INET;
            addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
            addr.sin_port = htons(0);  // random port

            len = sizeof(addr);
            if (bind(sfd, (struct sockaddr *) &addr, len)) {
                perror("bind()");
                goto fail;
            }

            if (getsockname(sfd, (struct sockaddr *) &addr, &len)) {
                perror("getsockname()");
                goto fail;
            }

            port = ntohs(addr.sin_port);

        fail:
            close(sfd);
            return port;
        }
__EOF__

    $freeport;
}


help_unit_json_ins()
{
cat <<__EOF__ ;
SYNOPSIS
        $0 json-ins [-hn] JSON INDEX

ARGUMENTS
        JSON    Path to a JSON file containing a top-level array.

        INDEX   Position in the array to insert the element at.

DESCRIPTION
        Insert a JSON element read from standard input into a JSON array read
        from a file at a given INDEX.

        The resulting array is printed to standard output.

OPTIONS
        -h, --help
                Print this help.

        -n, --dry-run
                Dry run.  Print the command to be run instead of actually
                running it.

__EOF__
}


unit_json_ins()
{
    while test $# -ge 1; do
        case "$1" in
        -h | --help)
            help_unit_json_ins;
            exit 0;
            ;;
        -n | --dry-run)
            dry_run='yes';
            ;;
        -*)
            err "json-ins: $1: Unknown option.";
            ;;
        *)
            break;
            ;;
        esac;
        shift;
    done;

    if ! test $# -ge 1; then
        err 'json-ins: JSON: Missing argument.';
    fi;
    local arr=$1;

    if ! test $# -ge 2; then
        err 'json-ins: INDEX: Missing argument.';
    fi;
    local idx=$2;

    dry_run_eval "(
        jq '.[0:$idx]' <'$arr';
        echo '[';
        jq .;
        echo ']';
        jq '.[$idx:]' <'$arr';
    ) \\
    | sed '/^\[]$/d' \\
    | sed '/^]$/{N;s/^]\n\[$/,/}' \\
    | jq .;"
}


help_unit_os_probe()
{
    cat <<__EOF__ ;
SYNOPSIS
        $0 os-probe [-h]

DESCRIPTION
        This script probes the OS and prints three fields, delimited by ':';
        the first is the package manager, the second is the OS name, the third
        is the OS version.

OPTIONS
        -h, --help
                Print this help.

__EOF__
}


unit_os_probe()
{
    while test $# -ge 1; do
        case "$1" in
        -h | --help)
            help_unit_os_probe;
            exit 0;
            ;;
        -*)
            err "os-probe: $1: Unknown option.";
            ;;
        *)
            err "os-probe: $1: Unknown argument.";
            ;;
        esac;
        shift;
    done;

    local os=$(uname | tr '[:upper:]' '[:lower:]')

    if [ "$os" != 'linux' ] && [ "$os" != 'freebsd' ]; then
        err "os-probe: The OS isn't Linux or FreeBSD; can't proceed."
    fi

    if [ "$os" = 'linux' ]; then
        if command -v apt-get >/dev/null; then
            local pkgMngr='apt';
        elif command -v dnf >/dev/null; then
            local pkgMngr='dnf';
        elif command -v yum >/dev/null; then
            local pkgMngr='yum';
        else
            local pkgMngr='';
        fi;

        local osRelease='/etc/os-release';

        if [ -f "$osRelease" ]; then
            # The value for the ID and VERSION_ID may or may not be in quotes
            local osName=$(grep "^ID=" "$osRelease" | sed s/\"//g | awk -F= '{ print $2 }' ||:)
            local osVersion=$(grep '^VERSION_ID=' "$osRelease" | sed s/\"//g | awk -F= '{ print $2 }' || lsb_release -cs)
        else
            err "os-probe: Unable to determine OS and version, or the OS isn't supported."
        fi
    else
        local pkgMngr='pkg';
        local osName=$os
        local osVersion=$(uname -rs | awk -F '[ -]' '{print $2}' ||:)
        if [ -z "$osVersion" ]; then
            err 'os-probe: Unable to get the FreeBSD version.'
        fi
    fi

    osName=$(echo "$osName" | tr '[:upper:]' '[:lower:]')
    echo "$pkgMngr:$osName:$osVersion"
}


help_unit_ps()
{
    cat <<__EOF__ ;
SYNOPSIS
        $0 ps [-h] [-t TYPE]

DESCRIPTION
        List unitd(8) processes.

OPTIONS
        -h, --help
                Print this help.

        -t, --type TYPE
                List only processes of type TYPE.  The available types are:

                -  controller (c)
                -  main (m)
                -  router (r)

__EOF__
}


unit_ps()
{
    while test $# -ge 1; do
        case "$1" in
        -h | --help)
            help_unit_ps;
            exit 0;
            ;;
        -t | --type)
            if ! test $# -ge 2; then
                err "ps: $1: Missing argument.";
            fi;
            local type=;
            case "$2" in
            c | controller)
                local type_c='c';
                ;;
            m | main)
                local type_m='m';
                ;;
            r | router)
                local type_r='r';
                ;;
            esac;
            shift;
            ;;
        -*)
            err "ps: $1: Unknown option.";
            ;;
        *)
            err "ps: $1: Unknown argument.";
            ;;
        esac;
        shift;
    done;

    ps awwx \
    | if test -v type; then
        grep ${type_c:+-e 'unit: controller'} \
             ${type_m:+-e 'unit: main'} \
             ${type_r:+-e 'unit: router'};
    else
        grep 'unit: ';
    fi \
    | grep -v grep \
    ||:
}


help_unit_repo_config()
{
    cat <<__EOF__ ;
SYNOPSIS
        $0 repo-config [-hn] [PKG-MANAGER OS-NAME OS-VERSION]

DESCRIPTION
        This script configures the NGINX Unit repository for the system
        package manager.

        The script automatically detects the OS and proceeds accordingly.
        However, if this automatic selection fails, you may specify the
        package manager and the OS name and version.

ARGUMENTS
        PKG-MANAGER
                Supported: 'apt', 'dnf', and 'yum'.

        OS-NAME
                Supported: 'debian', 'ubuntu', 'fedora', 'rhel', and 'amzn2'.

        OS-VERSION
                For most distributions, this should be a numeric value; for
                Debian derivatives, use the codename instead.

OPTIONS
        -h, --help
                Print this help.

        -n, --dry-run
                Dry run.  Print the commands to be run instead of actually
                running them.  Each command is preceded by a line explaining
                what it does.

EXAMPLES
        $ $(basename "$0") repo-config apt debian bullseye;
        $ $(basename "$0") repo-config apt ubuntu jammy;
        $ $(basename "$0") repo-config dnf fedora 36;
        $ $(basename "$0") repo-config dnf rhel 9;
        $ $(basename "$0") repo-config yum amzn2 2;

__EOF__
}


unit_repo_config()
{
    installAPT ()
    {
        local os_name="$2";

        dry_run_echo "Install on $os_name";
        dry_run_echo;
        dry_run_eval 'curl --output /usr/share/keyrings/nginx-keyring.gpg https://unit.nginx.org/keys/nginx-keyring.gpg;';
        dry_run_echo;
        dry_run_eval 'apt-get install -y apt-transport-https lsb-release ca-certificates;';

        if test $# -ge 3; then
            local os_version="$3";
        else
            local os_version='$(lsb_release -cs)';
        fi;

        dry_run_echo;
        dry_run_eval "printf 'deb [signed-by=/usr/share/keyrings/nginx-keyring.gpg] https://packages.nginx.org/unit/$os_name/ %s unit\n' \"$os_version\" | tee /etc/apt/sources.list.d/unit.list;";
        dry_run_eval "printf 'deb-src [signed-by=/usr/share/keyrings/nginx-keyring.gpg] https://packages.nginx.org/unit/$os_name/ %s unit\n' \"$os_version\" | tee -a /etc/apt/sources.list.d/unit.list;";
        dry_run_echo;
        dry_run_eval 'apt-get update;';
    }

    installYumDnf ()
    {
        local pkg_mngr="$1";
        local os_name="$2";

        if test $# -ge 3; then
            local os_version="$3";
        else
            local os_version='\$releasever';
        fi;

        dry_run_echo "Install on $os_name";
        dry_run_echo;

        dry_run_eval "cat >/etc/yum.repos.d/unit.repo <<__EOF__
[unit]
name=unit repo
baseurl=https://packages.nginx.org/unit/$os_name/$os_version/\\\$basearch/
gpgcheck=0
enabled=1
__EOF__";

        dry_run_echo;
        dry_run_eval "$pkg_mngr makecache;";
    }

    while test $# -ge 1; do
        case "$1" in
        -h | --help)
            help_unit_repo_config;
            exit 0;
            ;;
        -n | --dry-run)
            dry_run='yes';
            ;;
        -*)
            err "repo-config: $1: Unknown option.";
            ;;
        *)
            break;
            ;;
        esac;
        shift;
    done;

    if test $# -ge 1; then
        local pkg_mngr="$1";

        if ! test $# -ge 2; then
            err "repo-config: OS-NAME: Missing argument.";
        fi;
        local os_name="$2";

        if ! test $# -ge 3; then
            err "repo-config: OS-VERSION: Missing argument.";
        fi;
        local os_version="$3";
    fi;

    command -v curl >/dev/null \
    || err 'repo-config: curl(1) not found in PATH.  It must be installed to run this script.';

    echo 'This script sets up the NGINX Unit repository';

    if ! test $# -ge 3; then
        local os_pkg_name_version=$(unit_os_probe || warn "On macOS, try 'brew install nginx/unit/unit'.")
        local pkg_mngr=$(echo "$os_pkg_name_version" | awk -F: '{print $1}')
        local os_name=$(echo "$os_pkg_name_version" | awk -F: '{print $2}')
        local os_version=$(echo "$os_pkg_name_version" | awk -F: '{print $3}')
    fi;

    # Call the appropriate installation function
    case "$pkg_mngr" in
    apt)
        case "$os_name" in
        debian | ubuntu)
            installAPT "$pkg_mngr" "$os_name" ${3:+$os_version};
            ;;
        *)
            err "repo-config: $os_name: The OS isn't supported.";
            ;;
        esac
        ;;
    yum | dnf)
        case "$os_name" in
        rhel | amzn | fedora)
            installYumDnf "$pkg_mngr" "$os_name" "$os_version" ${3:+ovr};
            ;;
        *)
            err "repo-config: $os_name: The OS isn't supported.";
            ;;
        esac;
        ;;
    *)
        err "repo-config: $pkg_mngr: The package manager isn't supported.";
        ;;
    esac;

    echo
    echo 'All done; the NGINX Unit repository is set up.';
    echo "Configured with '$pkg_mngr' on '$os_name' '$os_version'.";
    echo 'Further steps: <https://unit.nginx.org/installation/#official-packages>'
}


help_unit_restart()
{
    cat <<__EOF__ ;
SYNOPSIS
        $0 restart [-hls]

DESCRIPTION
        Restart all running unitd(8) instances.

OPTIONS
        -h, --help
                Print this help.

        -l, --log
                Reset log file.

        -s, --statedir
                Reset \$statedir.

CAVEATS
        This command will ask for confirmation before removing
        directories; please review those prompts with care, as unknown
        bugs in the command may attempt to wipe your file system.

__EOF__
}


unit_restart()
{
    while test $# -ge 1; do
        case "$1" in
        -h | --help)
            help_unit_restart;
            exit 0;
            ;;
        -l | --log)
            local log_flag='yes';
            ;;
        -s | --statedir)
            local state_flag='yes';
            ;;
        -*)
            err "restart: $1: Unknown option.";
            ;;
        *)
            err "restart: $1: Unknown argument.";
            ;;
        esac;
        shift;
    done;

    local cmds="$(unit_cmd)";

    pkill -e unitd;

    printf '%s\n' "$cmds" \
    | while read -r cmd; do
        if test -v log_flag; then
            (
                echo "$cmd" \
                | grep '\--log' \
                | sed 's/.*--log \+\([^ ]\+\).*/\1/' \
                || eval $cmd --help \
                | grep -A1 '\--log FILE' \
                | grep 'default:' \
                | sed 's/.*"\(.*\)".*/\1/';
            ) \
            | xargs rm -f;
        fi;

        if test -v state_flag; then
            (
                echo "$cmd" \
                | grep '\--statedir' \
                | sed 's/.*--statedir \+\([^ ]\+\).*/\1/' \
                || eval $cmd --help \
                | grep -A1 '\--statedir DIR' \
                | grep 'default:' \
                | sed 's/.*"\(.*\)".*/\1/';
            ) \
            | xargs -I {} find {} -mindepth 1 -maxdepth 1 \
            | xargs rm -rfi;
        fi;

        eval $cmd;
    done;
}


help_unit_sock()
{
    cat <<__EOF__ ;
SYNOPSIS
        $0 sock [-h] SUBCOMMAND [ARGS]

    Subcommands
        ├── filter  [-ch]
        └── find    [-h]

DESCRIPTION
        Print the control API socket address of running unitd(8)
        instances.

        Run '$0 sock SUBCOMMAND -h' for more information on a
        subcommand.

SUBCOMMANDS
        filter  Filter the output of the 'find' subcommand and transform it
                to something suitable for running other commands, such as
                curl(1) or ssh(1).

        find    Find and print the control API socket address of running
                unitd(8) instances.

OPTIONS
        -h, --help
                Print this help.

__EOF__
}


unit_sock()
{
    while test $# -ge 1; do
        case "$1" in
        -h | --help)
            help_unit_sock;
            exit 0;
            ;;
        -*)
            err "sock: $1: Unknown option.";
            ;;
        *)
            break;
            ;;
        esac;
        shift;
    done;

    if ! test $# -ge 1; then
        err 'sock: Missing subcommand.';
    fi;

    case $1 in
    filter)
        shift;
        unit_sock_filter $@;
        ;;
    find)
        shift;
        unit_sock_find $@;
        ;;
    *)
        err "sock: $1: Unknown subcommand.";
        ;;
    esac;
}


help_unit_sock_filter()
{
    cat <<__EOF__ ;
SYNOPSIS
        $0 sock filter [-chs]

DESCRIPTION
        Filter the output of the 'sock find' command and transform it to
        something suitable for running other commands, such as
        curl(1) or ssh(1).

OPTIONS
        -c, --curl
                Print an argument suitable for curl(1).

        -h, --help
                Print this help.

        -s, --ssh
                Print a socket address suitable for use in an ssh(1) tunnel.

__EOF__
}


unit_sock_filter()
{
    while test $# -ge 1; do
        case "$1" in
        -c | --curl)
            if test -v ssh_flag; then
                err "sock: filter: $1: Missing argument.";
            fi;
            local curl_flag='yes';
            ;;
        -h | --help)
            help_unit_sock_filter;
            exit 0;
            ;;
        -s | --ssh)
            if test -v curl_flag; then
                err "sock: filter: $1: Missing argument.";
            fi;
            local ssh_flag='yes';
            ;;
        -*)
            err "sock: filter: $1: Unknown option.";
            ;;
        *)
            err "sock: filter: $1: Unknown argument.";
            ;;
        esac;
        shift;
    done;

    while read -r control; do

        if test -v curl_flag; then
            if echo "$control" | grep '^unix:' >/dev/null; then
                unix_socket="$(echo "$control" | sed 's/unix:/--unix-socket /')";
                host='http://localhost';
            else
                unix_socket='';
                host="$control";
            fi;

            echo "$unix_socket $host";

        elif test -v ssh_flag; then
            echo "$control" \
            | sed -E 's,^(unix:|http://|https://),,';

        else
            echo "$control";
        fi;
    done;
}


help_unit_sock_find()
{
    cat <<__EOF__ ;
SYNOPSIS
        $0 sock find [-h]

DESCRIPTION
        Find and print the control API socket address of running
        unitd(8) instances.

OPTIONS
        -h, --help
                Print this help.

__EOF__
}


unit_sock_find()
{
    while test $# -ge 1; do
        case "$1" in
        -h | --help)
            help_unit_sock_find;
            exit 0;
            ;;
        -*)
            err "sock: find: $1: Unknown option.";
            ;;
        *)
            err "sock: find: $1: Unknown argument.";
            ;;
        esac;
        shift;
    done;

    unit_cmd \
    | while read -r cmd; do
        if echo "$cmd" | grep '\--control' >/dev/null; then
            echo "$cmd" \
            | sed 's/ --/\n--/g' \
            | grep '\--control' \
            | cut -d' ' -f2;
        else
            if ! command -v $cmd >/dev/null; then
                local cmd='unitd';
            fi;
            $cmd --help \
            | sed -n '/\--control/,+1p' \
            | grep 'default:' \
            | sed 's/ *default: "\(.*\)"/\1/';
        fi;
    done;
}


while test $# -ge 1; do
    case "$1" in
    -h | --help)
        help_unit;
        exit 0;
        ;;
    -hh | --help-more)
        help_more_unit;
        exit 0;
        ;;
    -*)
        err "$1: Unknown option.";
        ;;
    *)
        break;
        ;;
    esac;
    shift;
done;

if ! test $# -ge 1; then
    err "Missing command.";
fi;

case $1 in
cmd)
    shift;
    unit_cmd $@;
    ;;
ctl)
    shift;
    unit_ctl $@;
    ;;
freeport)
    shift;
    unit_freeport $@;
    ;;
json-ins)
    shift;
    unit_json_ins $@;
    ;;
os-probe)
    shift;
    unit_os_probe $@;
    ;;
ps)
    shift;
    unit_ps $@;
    ;;
repo-config)
    shift;
    unit_repo_config $@;
    ;;
restart)
    shift;
    unit_restart $@;
    ;;
sock)
    shift;
    unit_sock $@;
    ;;
welcome)
    shift;
    unit_ctl_welcome $@;
    ;;
*)
    err "$1: Unknown command.";
    ;;
esac;
